| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- <template>
- <div class="admin--page-content">
- <div v-if="isLoading" class="admin--loading">데이터를 불러오는 중...</div>
- <div v-else class="admin--form">
- <!-- 관리자 정보 -->
- <table class="admin--form--table">
- <colgroup>
- <col style="width: 140px;">
- <col>
- </colgroup>
- <tbody>
- <tr>
- <th><div>아이디</div></th>
- <td class="admin--table-title">{{ data.username || "-" }}</td>
- </tr>
- <tr>
- <th><div>이름</div></th>
- <td>{{ data.name || "-" }}</td>
- </tr>
- <tr>
- <th><div>이메일</div></th>
- <td>{{ data.email || "-" }}</td>
- </tr>
- <tr>
- <th><div>핸드폰</div></th>
- <td>{{ data.phone || "-" }}</td>
- </tr>
- <tr>
- <th><div>권한</div></th>
- <td>
- <span v-if="data.role === 'super_admin'" class="admin--badge admin--badge-super">
- 슈퍼 관리자
- </span>
- <div v-else class="admin--perm-list">
- <span v-for="p in data.permissions" :key="p" class="admin--badge admin--badge-perm">
- {{ permLabel(p) }}
- </span>
- <span v-if="!data.permissions.length" class="admin--badge admin--badge-ended">
- 권한 없음
- </span>
- </div>
- </td>
- </tr>
- <tr>
- <th><div>상태</div></th>
- <td>
- <span :class="['admin--badge', getStatusBadgeClass(data.status)]">
- {{ getStatusLabel(data.status) }}
- </span>
- <span v-if="isLocked" class="admin--badge admin--badge-ended ml--16">🔒 잠김 (5회 실패)</span>
- </td>
- </tr>
- <tr>
- <th><div>최근 로그인</div></th>
- <td>{{ formatDateTime(data.last_login) }}</td>
- </tr>
- <tr>
- <th><div>로그인 실패 횟수</div></th>
- <td>
- {{ data.login_attempts || 0 }}회
- <span v-if="data.last_failed_login" class="ml--16">(최근 실패: {{ formatDateTime(data.last_failed_login) }})</span>
- </td>
- </tr>
- <tr>
- <th><div>등록일</div></th>
- <td>{{ formatDateTime(data.created_at) }}</td>
- </tr>
- <tr>
- <th><div>최근 수정</div></th>
- <td>{{ formatDateTime(data.updated_at) }}</td>
- </tr>
- </tbody>
- </table>
- <!-- 버튼 영역 -->
- <div class="admin--form-actions">
- <button type="button" class="admin--btn" @click="goToList">
- ← 목록으로
- </button>
- <button
- v-if="canModify && isLocked"
- type="button"
- class="admin--btn admin--btn-blue ml--auto"
- @click="handleUnlock"
- >🔓 잠금 해제</button>
- <button
- v-if="canModify"
- type="button"
- class="admin--btn admin--btn-blue-border"
- :class="{ 'ml--auto': !isLocked }"
- @click="openPasswordModal"
- >비밀번호 변경</button>
- <button v-if="canModify" type="button" class="admin--btn admin--btn-red-border" @click="handleDelete">
- 삭제
- </button>
- <button v-if="canModify" type="button" class="admin--btn admin--btn-red" @click="goToEdit">
- 수정
- </button>
- </div>
- </div>
- <!-- 비밀번호 변경 모달 -->
- <Teleport to="body">
- <div v-if="passwordModal.show" class="admin--modal-overlay" @click.self="closePasswordModal">
- <div class="admin--alert-modal admin--form-modal">
- <div class="admin--modal-header">
- <h4>🔒 비밀번호 변경</h4>
- <button class="admin--modal-close" @click="closePasswordModal">×</button>
- </div>
- <div class="admin--modal-body">
- <p class="admin--modal-target">대상 계정 <strong>{{ data.username }}</strong></p>
- <div class="admin--form-field">
- <label class="admin--form-label">새 비밀번호</label>
- <input
- v-model="passwordModal.newPassword"
- type="password"
- class="admin--form-input"
- placeholder="8자 이상"
- autocomplete="new-password"
- />
- </div>
- <div class="admin--form-field">
- <label class="admin--form-label">새 비밀번호 확인</label>
- <input
- v-model="passwordModal.confirmPassword"
- type="password"
- class="admin--form-input"
- placeholder="다시 입력"
- autocomplete="new-password"
- @keyup.enter="handleChangePassword"
- />
- </div>
- </div>
- <div class="admin--modal-footer">
- <button class="admin--btn" @click="closePasswordModal">취소</button>
- <button class="admin--btn admin--btn-red ml--auto" :disabled="passwordModal.isSaving" @click="handleChangePassword">
- {{ passwordModal.isSaving ? "변경 중..." : "변경하기" }}
- </button>
- </div>
- </div>
- </div>
- </Teleport>
- <!-- 알림 모달 -->
- <AdminAlertModal
- v-if="alertModal.show"
- :title="alertModal.title"
- :message="alertModal.message"
- :type="alertModal.type"
- @confirm="handleAlertConfirm"
- @cancel="handleAlertCancel"
- @close="closeAlertModal"
- />
- </div>
- </template>
- <script setup>
- import { ref, computed, onMounted } from "vue";
- import { useRoute, useRouter } from "vue-router";
- import AdminAlertModal from "~/components/admin/AdminAlertModal.vue";
- definePageMeta({
- layout: "admin",
- middleware: ["auth"],
- });
- const route = useRoute();
- const router = useRouter();
- const { get, post, del } = useApi();
- const { isSuperAdmin } = useAuth();
- const adminId = route.params.id;
- // 일반 admin은 슈퍼관리자 정보 수정/삭제/비번/잠금 불가
- const canModify = computed(() => isSuperAdmin.value || data.value.role !== "super_admin");
- const isLoading = ref(true);
- const data = ref({
- username: "",
- name: "",
- email: "",
- phone: "",
- role: "",
- status: "",
- permissions: [],
- login_attempts: 0,
- last_failed_login: "",
- last_login: "",
- created_at: "",
- updated_at: "",
- });
- // 권한 라벨 매핑 (admin.vue menuItems와 동일)
- const PERM_LABELS = {
- admin: "관리자",
- field: "분야/지역",
- fishing: "선상/낚시터",
- challenge: "챌린지",
- quest: "퀘스트",
- item: "아이템",
- species: "어종",
- user: "회원",
- };
- const permLabel = (id) => PERM_LABELS[id] || id;
- const isLocked = computed(() => (data.value.login_attempts || 0) >= 5);
- // 알림 모달
- const alertModal = ref({ show: false, title: "알림", message: "", type: "alert", onConfirm: null });
- const showAlert = (message, title = "알림") => {
- alertModal.value = { show: true, title, message, type: "alert", onConfirm: null };
- };
- const showConfirm = (message, onConfirm, title = "확인") => {
- alertModal.value = { show: true, title, message, type: "confirm", onConfirm };
- };
- const closeAlertModal = () => { alertModal.value.show = false; };
- const handleAlertConfirm = () => {
- if (alertModal.value.onConfirm) alertModal.value.onConfirm();
- closeAlertModal();
- };
- const handleAlertCancel = () => closeAlertModal();
- // 비밀번호 변경 모달
- const passwordModal = ref({ show: false, newPassword: "", confirmPassword: "", isSaving: false });
- const openPasswordModal = () => {
- passwordModal.value = { show: true, newPassword: "", confirmPassword: "", isSaving: false };
- };
- const closePasswordModal = () => {
- passwordModal.value.show = false;
- };
- // 상세 조회
- const loadDetail = async () => {
- isLoading.value = true;
- const { data: res, error } = await get(`/admin/${adminId}`);
- if (error || !res?.success) {
- showAlert(error?.message || res?.message || "조회에 실패했습니다.", "오류");
- isLoading.value = false;
- return;
- }
- const row = res.data || {};
- data.value = {
- username: row.username ?? "",
- name: row.name ?? "",
- email: row.email ?? "",
- phone: row.phone ?? "",
- role: row.role ?? "",
- status: row.status ?? "",
- permissions: Array.isArray(row.permissions) ? row.permissions : [],
- login_attempts: row.login_attempts ?? 0,
- last_failed_login: row.last_failed_login ?? "",
- last_login: row.last_login ?? "",
- created_at: row.created_at ?? "",
- updated_at: row.updated_at ?? "",
- };
- isLoading.value = false;
- };
- // 삭제
- const handleDelete = () => {
- showConfirm(
- `'${data.value.username}' 관리자를 삭제하시겠습니까?`,
- async () => {
- const { data: res, error } = await del(`/admin/${adminId}`);
- if (error || !res?.success) {
- showAlert(error?.message || res?.message || "삭제에 실패했습니다.", "오류");
- } else {
- showAlert(res.message || "삭제되었습니다.", "성공");
- setTimeout(() => router.push("/site-manager/admin/list"), 800);
- }
- },
- "관리자 삭제"
- );
- };
- // 잠금 해제
- const handleUnlock = () => {
- showConfirm(
- `'${data.value.username}' 계정의 잠금을 해제하시겠습니까?`,
- async () => {
- const { data: res, error } = await post(`/admin/${adminId}/unlock`, {});
- if (error || !res?.success) {
- showAlert(error?.message || res?.message || "잠금 해제에 실패했습니다.", "오류");
- } else {
- showAlert(res.message || "잠금이 해제되었습니다.", "성공");
- await loadDetail();
- }
- },
- "잠금 해제"
- );
- };
- // 비밀번호 변경
- const handleChangePassword = async () => {
- const pw = passwordModal.value.newPassword;
- const confirm = passwordModal.value.confirmPassword;
- if (!pw || pw.length < 8) {
- showAlert("비밀번호는 8자 이상 입력하세요.", "입력 오류");
- return;
- }
- if (pw !== confirm) {
- showAlert("비밀번호가 일치하지 않습니다.", "입력 오류");
- return;
- }
- passwordModal.value.isSaving = true;
- const { data: res, error } = await post(`/admin/${adminId}/password`, { new_password: pw });
- passwordModal.value.isSaving = false;
- if (error || !res?.success) {
- showAlert(error?.message || res?.message || "비밀번호 변경에 실패했습니다.", "오류");
- } else {
- closePasswordModal();
- showAlert(res.message || "비밀번호가 변경되었습니다.", "성공");
- }
- };
- // 이동
- const goToList = () => router.push("/site-manager/admin/list");
- const goToEdit = () => router.push(`/site-manager/admin/edit/${adminId}`);
- // 라벨 / 뱃지
- const getStatusLabel = (status) => {
- if (status === "active") return "활성";
- if (status === "inactive") return "휴면";
- if (status === "suspended") return "정지";
- return "-";
- };
- const getStatusBadgeClass = (status) => {
- if (status === "active") return "admin--badge-active";
- if (status === "inactive") return "admin--badge-ended";
- if (status === "suspended") return "admin--badge-sus";
- return "";
- };
- // 일시 포맷 (24시)
- const formatDateTime = (dateString) => {
- if (!dateString) return "-";
- const date = new Date(dateString.replace(" ", "T"));
- if (isNaN(date.getTime())) return dateString;
- return date.toLocaleString("ko-KR", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- hour12: false,
- });
- };
- onMounted(() => {
- loadDetail();
- });
- </script>
|